package org.springframework.security.web.access;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;

import java.io.IOException;

/**
 * @author Dillon
 * @date 2024/6/30
 * @slogan 致敬大师 致敬未来的你
 * @desc springSecurity 处理异常过滤器
 * 主要处理 认证异常 {@link AuthenticationException}
 * 授权异常 {@link AccessDeniedException}
 * 根据异常分发不同的处理
 */
public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {

	/**
	 * 认证上下文缓存
	 */
	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();

	/**
	 * 授权失败处理器 授权失败后 怎么处理
	 */
	private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

	/**
	 * 认证失败处理器 认证失败后 怎么处理
	 * 这里使用的是代理认证失败处理器 代理里面通过请求匹配器查找到真正处理器，然后调用
	 */
	private final AuthenticationEntryPoint authenticationEntryPoint;

	/**
	 * 身份信息验证解析器 解析是否为匿名 是否为记住我等
	 */
	private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();

	/**
	 * 异常解析器 从异常链中获取需要的异常
	 */
	private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();

	/**
	 * 请求缓存器 缓存当前请求
	 */
	private final RequestCache requestCache;

	/**
	 * 国际化消息处理器，spring框架中的类 配合 i18n 处理国际化消息
	 */
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	/**
	 * 构造函数
	 *
	 * @param authenticationEntryPoint 认证失败处理器
	 */
	public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
		this(authenticationEntryPoint, new HttpSessionRequestCache());
	}

	/**
	 * 构造函数
	 *
	 * @param authenticationEntryPoint 认证失败处理器
	 * @param requestCache             请求缓存
	 */
	public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint, RequestCache requestCache) {
		Assert.notNull(authenticationEntryPoint, "认证失败处理器不能为空");
		Assert.notNull(requestCache, "请求缓存不能为空");
		this.authenticationEntryPoint = authenticationEntryPoint;
		this.requestCache = requestCache;
	}

	/**
	 * 后置处理 校验认证处理器不能为空
	 */
	@Override
	public void afterPropertiesSet() {
		Assert.notNull(this.authenticationEntryPoint, "登录认证处理器不能为空");
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

	/**
	 * 过滤处理
	 *
	 * @param request  请求对象
	 * @param response 响应对象
	 * @param chain    过滤器链
	 * @throws IOException      执行异常
	 * @throws ServletException 执行异常
	 */
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
		try {
			// 默认跳过，主要根据异常来进行特殊处理
			chain.doFilter(request, response);
		} catch (IOException ex) {
			// IO异常不属于可控异常，直接抛出
			throw ex;
		} catch (Exception ex) {
			// 尝试解析出额需要解析的异常
			Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
			// 尝试判断是否为认证异常
			RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
			// 尝试判断是否为权限认证异常
			if (securityException == null) {
				securityException = (AccessDeniedException) this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}
			// 两个都不是，则继续抛出异常
			if (securityException == null) {
				rethrow(ex);
			}
			// 响应状态提交临界判断
			if (response.isCommitted()) {
				throw new ServletException("不能处理的Security异常，因为 响应已经提交", ex);
			}
			// 处理资源权限认证和登录认证
			handleSpringSecurityException(request, response, chain, securityException);
		}
	}

	/**
	 * 继续抛出异常
	 *
	 * @param ex 异常
	 * @throws ServletException 执行失败
	 */
	private void rethrow(Exception ex) throws ServletException {
		// 如果是 ServletException 异常，则抛出 ServletException 保证 servlet处理正确
		if (ex instanceof ServletException) {
			throw (ServletException) ex;
		}
		// 如果是运行时异常，则继续抛出运行时异常
		if (ex instanceof RuntimeException) {
			throw (RuntimeException) ex;
		}
		// 其他异常也被包装至运行时异常抛出
		throw new RuntimeException(ex);
	}

	/**
	 * 获取认证失败处理器
	 *
	 * @return 认证失败处理器
	 */
	public AuthenticationEntryPoint getAuthenticationEntryPoint() {
		return this.authenticationEntryPoint;
	}

	/**
	 * 获取请求解析器 解析请求是匿名 还是记住我
	 *
	 * @return 请求解析器
	 */
	protected AuthenticationTrustResolver getAuthenticationTrustResolver() {
		return this.authenticationTrustResolver;
	}

	/**
	 * 处理登录认证和授权认证
	 *
	 * @param request   请求对象
	 * @param response  响应对象
	 * @param chain     过滤器链
	 * @param exception 异常对现象
	 * @throws IOException      执行异常
	 * @throws ServletException 执行异常
	 */
	private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, RuntimeException exception) throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
		} else if (exception instanceof AccessDeniedException) {
			handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
		}
	}

	/**
	 * 处理登录认证
	 *
	 * @param request   请求对象
	 * @param response  响应对象
	 * @param chain     过滤器链
	 * @param exception 登录认证异常
	 * @throws ServletException 执行异常
	 * @throws IOException      执行异常
	 */
	private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
		this.logger.trace("登录认证失败，向认证处理器发送认证请求", exception);
		sendStartAuthentication(request, response, chain, exception);
	}

	/**
	 * 处理授权认证
	 *
	 * @param request   请求对象
	 * @param response  响应对象
	 * @param chain     过滤器链
	 * @param exception 授权异常
	 * @throws ServletException 执行异常
	 * @throws IOException      执行异常
	 */
	private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
		// 获取认证实现类
		Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
		// 判断是否为匿名请求（不需要登录） 或者是记住我请求
		boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
		boolean rememberMe = this.authenticationTrustResolver.isRememberMe(authentication);
		// 如果是匿名 或者记住我，走if 其他走 else
		if (isAnonymous || rememberMe) {
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("认证失效 %s 向认证发送请求", authentication), exception);
			}
			// 执行登录认证
			sendStartAuthentication(request, response, chain,
					new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "当前请求需要认证")));
		} else {
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("授权失效 %s to 向认证发送请求", authentication), exception);
			}
			// 执行授权处理
			this.accessDeniedHandler.handle(request, response, exception);
		}
	}

	/**
	 * 登录认证处理 委托给 AuthenticationEntryPoint 处理
	 *
	 * @param request  请求对象
	 * @param response 响应对象
	 * @param chain    过滤器链
	 * @param reason   认证异常
	 * @throws ServletException 执行异常
	 * @throws IOException      执行异常
	 */
	protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		// 初始化一个空的登录认证标记对象
		SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
		this.securityContextHolderStrategy.setContext(context);
		// 记录请求缓存
		this.requestCache.saveRequest(request, response);
		// 执行登录认证处理
		this.authenticationEntryPoint.commence(request, response, reason);
	}

	/**
	 * 设置授权失败处理器
	 *
	 * @param accessDeniedHandler 授权失败处理器
	 */
	public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
		Assert.notNull(accessDeniedHandler, "授权失败处理器不能为空");
		this.accessDeniedHandler = accessDeniedHandler;
	}

	/**
	 * 设置认证请求解析器
	 *
	 * @param authenticationTrustResolver 认证请求解析器
	 */
	public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
		Assert.notNull(authenticationTrustResolver, "认证请求解析器 不能为空");
		this.authenticationTrustResolver = authenticationTrustResolver;
	}

	/**
	 * 设置异常分析器
	 *
	 * @param throwableAnalyzer 异常分析器
	 */
	public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
		Assert.notNull(throwableAnalyzer, "异常分析器不能为空");
		this.throwableAnalyzer = throwableAnalyzer;
	}

	/**
	 * 设置国际化消息处理器
	 *
	 * @param messageSource 国际化消息
	 */
	@Override
	public void setMessageSource(MessageSource messageSource) {
		Assert.notNull(messageSource, "国际化消息不能为空");
		this.messages = new MessageSourceAccessor(messageSource);
	}

	/**
	 * 设置认证缓存策略对象
	 *
	 * @param securityContextHolderStrategy 认证缓存策略
	 */
	public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
		Assert.notNull(securityContextHolderStrategy, "认证缓存策略不能为空");
		this.securityContextHolderStrategy = securityContextHolderStrategy;
	}

	/**
	 * 默认的异常解析器
	 * 继承自 ThrowableAnalyzer
	 */
	private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {

		/**
		 * 设置额外解析的异常class
		 * ServletException
		 */
		@Override
		protected void initExtractorMap() {
			super.initExtractorMap();
			registerExtractor(ServletException.class, (throwable) -> {
				ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
				return ((ServletException) throwable).getRootCause();
			});
		}

	}

}
